/**
 * @namespace Cross-browser registration of event handlers, automatically
 *            normalizes the event object to provide web standard features such
 *            as preventDefault() and stopPropagation().
 */
Jelo.Event = function() {
    
    /** @private convenience */
    var D = document;
    
    /** @private */
    var isDomReady = false;
    
    /** @private */
    var domFunctions = [];
    
    /** @private */
    var handlers = [];
    
    /** @private */
    var fireDomReady = function() {
        isDomReady = true;
        for (var i = 0; i < domFunctions.length; i++) {
            var fn = domFunctions[i];
            try {
                fn();
            } catch (e) {
                // TODO: log these internally so they can be shown if the developer desires
            }
        }
        domFunctions = [];
    };
    
    /** @private */
    var init = function() {
        if (D.addEventListener) {
            D.addEventListener("DOMContentLoaded", fireDomReady, false);
        } else if (Jelo.Env.isIE) {
            D.write("<script id='ieDomReady' defer " + "src=//:><\/script>");
            var ieReady = D.getElementById("ieDomReady");
            ieReady.onreadystatechange = function() {
                if (ieReady.readyState === "complete") {
                    fireDomReady();
                }
            };
        }
        if (Jelo.Env.isWebkit) {
            var timerDomReady = setInterval(function() {
                if (/loaded|complete/i.test(D.readyState)) {
                    fireDomReady.call(this);
                    clearInterval(timerDomReady);
                }
            }, 10);
        } else {
            var oldOnload = (typeof window.onload == "function") ? window.onload : function() {};
            window.onload = function() {
                oldOnload();
                fireDomReady();
            };
        }
    }();
    
    function find(el, ev, fn) {
        var handlers = el._handlers;
        if (!handlers) {
            return -1;
        }
        var d = el.document || el;
        var w = d.parentWindow;
        for (var i = handlers.length - 1; i >= 0; i--) {
            var a = w._allHandlers[handlers[i]];
            if (a.eventType == ev && a.handler == fn) {
                return i;
            }
        }
        return -1;
    }
    
    function removeAllHandlers() {
        var w = this;
        var wa = w._allHandlers;
        for (var id in wa) {
            if (wa.hasOwnProperty(id)) {
                var h = wa[id];
                h.element.detachEvent('on' + h.eventType, h.wrappedHandler);
                delete wa[id];
            }
        }
    }
    
    /** @scope Jelo.Event */
    return {
        /**
         * Fires after all elements in the document is available. In most cases,
         * this function may be executed before window.onload and before images
         * fully load, which can result in a faster page response time.
         * 
         * @see Jelo#onReady
         */
        onReady   : function(fn) {
            if (typeof fn === "function") {
                if (isDomReady) {
                    fn();
                } else {
                    domFunctions.push(fn);
                }
            }
        },
        /**
         * Start listening for an event. Multiple listeners can be registered to
         * a single element. Can be accessed via {@link Jelo.on}.
         * 
         * @param {HTMLElement} element HTML element to which Jelo should
         *        listen.
         * @param {String} eventName The type of event for which Jelo should
         *        listen, such as "click" or "mouseup". Should be all lowercase,
         *        and WITHOUT the IE prefix "on".
         * @param {Function} handler Method to be invoked when the event occurs.
         *        The execution scope ("this") will be the actual element that
         *        caught the event which, due to the DOM hierarchy, may be a
         *        child of the element registered via this function. The
         *        function is passed the following arguments:
         *        <ul>
         *        <li>target: The element that caught the event.</li>
         *        <li>event: The event object, normalized to conform to W3C
         *        standards.</li>
         *        </ul>
         */
        add       : function(el, ev, fn) {
            if (Jelo.Valid.isArray(el)) {
                Jelo.each(el, function() {
                    Jelo.Event.add(this, ev, fn);
                });
                return;
            }
            if (!!el && (typeof ev == 'string') && (typeof fn == 'function')) {
                var handler = function(e) {
                    e = e || window.event;
                    var event = {};
                    var properties = ['type', 'shiftKey', 'ctrlKey', 'altKey', 'keyCode',
                        'charCode', 'button', 'which', 'clientX', 'clientY', 'mouseX', 'mouseY',
                        'metaKey', 'pageX', 'pageY', 'screenX', 'screenY', 'relatedTarget'];
                    var len = properties.length;
                    for (var i = 0; i < len; i++) {
                        event[properties[i]] = e[properties[i]];
                    }
                    target = e.target ? e.target : e.srcElement;
                    if (target.nodeType === 3) {
                        target = target.parentNode;
                    }
                    event.preventDefault = function() {
                        if (e.preventDefault) {
                            e.preventDefault();
                        } else {
                            e.returnValue = false;
                        }
                    };
                    event.stopPropagation = function() {
                        if (e.stopPropagation) {
                            e.stopPropagation();
                        } else {
                            e.cancelBubble = true;
                        }
                    };
                    fn.call(target, target, event);
                };
                if (el.addEventListener) {
                    el.addEventListener(ev, handler, false);
                } else if (el.attachEvent) {
                    el.attachEvent("on" + ev, handler);
                } else {
                    throw ("Jelo.Event.add: Could not observe " + ev + " on " + el);
                }
                var newEvent = {
                    target  : el,
                    event   : ev,
                    handler : handler,
                    fn      : fn
                };
                handlers.push(newEvent);
            } else {
                throw ("Syntax Error. Jelo.Event.add(DOMElement, Event:String, Function)");
            }
        },
        /**
         * Stop listening for an event. Safe to call even if no such listener
         * has been registered. Can be accessed via {@link Jelo.un}.
         * 
         * @param {HTMLElement} element HTML element to which Jelo should
         *        listen.
         * @param {String} eventName The type of event for which Jelo should
         *        listen, such as "click" or "mouseup". Should be all lowercase,
         *        and WITHOUT the IE prefix "on".
         * @param {Function} handler Method to be invoked when the event occurs.
         *        Anonymous handlers cannot be unregistered at this time.
         */
        remove    : function(el, ev, fn) {
            if (Jelo.Valid.isArray(el)) {
                Jelo.each(el, function() {
                    Jelo.Event.remove(this, ev, fn);
                });
                return;
            }
            if (!!el && (typeof ev == 'string') && (typeof fn == 'function')) {
                for (var i = 0; i < handlers.length; i++) {
                    var jeh = handlers[i];
                    var ml = el === jeh.target;
                    var mv = ev === jeh.event;
                    var mf = fn === jeh.fn;
                    if (ml && mv && mf) {
                        if (el.removeEventListener) {
                            el.removeEventListener(ev, jeh.handler, false);
                        } else if (el.detachEvent) {
                            el.detachEvent("on" + ev, jeh.handler);
                        } else {
                            throw ("Jelo.Event.remove: Could not remove " + ev + " from " + el);
                        }
                        handlers.splice(i, 1);
                        break;
                    }
                }
            } else {
                throw ("Syntax Error. Jelo.Event.remove(DOMElement, Event:String, Function");
            }
        },
        
        /**
         * @deprecated
         * Bootstrap the next version of Jelo.Event.add and Jelo.Event.remove.
         * The only difference you should notice as a developer is that the arguments
         * passed to the OLD event handler were (target, event). The NEW arguments
         * are (event) to fit in line with most implementations out there.
         * See also: {@link Jelo.Event.isFixed}.
         */
        normalize : function() {
            Jelo.Event.add = (D.addEventListener) ? function(el, ev, fn) {
                if (Jelo.Valid.isArray(el)) {
                    Jelo.each(el, function() {
                        Jelo.Event.add(this, ev, fn);
                    });
                    return;
                }
                el.addEventListener(ev, fn, false);
            } : function(el, ev, fn) {
                if (Jelo.Valid.isArray(el)) {
                    Jelo.each(el, function() {
                        Jelo.Event.add(this, ev, fn);
                    });
                    return;
                }
                if (find(el, ev, fn) != -1) {
                    return;
                }
                var wh = function(e) {
                    e = e || window.event;
                    var event = {
                        _event          : e,
                        type            : e.type,
                        target          : e.srcElement,
                        currentTarget   : el,
                        relatedTarget   : e.fromElement ? e.fromElement : e.toElement,
                        eventPhase      : (e.srcElement == el) ? 2 : 3,
                        clientX         : e.clientX,
                        clientY         : e.clientY,
                        screenX         : e.screenX,
                        screenY         : e.screenY,
                        altKey          : e.altKey,
                        ctrlKey         : e.ctrlKey,
                        shiftKey        : e.shiftKey,
                        charCode        : e.charCode || e.keyCode,
                        keyCode         : e.keyCode || e.charCode,
                        button          : e.button ? {
                            1 : 0,
                            4 : 1,
                            2 : 2
                        }[e.button] : (e.which ? e.which - 1 : -1),
                        which           : e.which || e.button,
                        stopPropagation : function() {
                            this._event.cancelBubble = true;
                        },
                        preventDefault  : function() {
                            this._event.returnValue = false;
                        }
                    };
                    fn.call(el, event);
                };
                el.attachEvent('on' + ev, wh);
                var h = {
                    element        : el,
                    eventType      : ev,
                    handler        : fn,
                    wrappedHandler : wh
                };
                var d = el.document || el;
                var w = d.parentWindow;
                var id = 'h' + Jelo.uID();
                if (!w._allHandlers) {
                    w._allHandlers = {};
                }
                w._allHandlers[id] = h;
                if (!el._handlers) {
                    el._handlers = [];
                }
                el._handlers.push(id);
                if (!w._onunloadRegistered) {
                    w.attachEvent('onunload', removeAllHandlers);
                    w._onunloadRegistered = true;
                }
            };
            Jelo.Event.remove = (D.removeEventListener) ? function(el, ev, fn) {
                if (Jelo.Valid.isArray(el)) {
                    Jelo.each(el, function() {
                        Jelo.Event.remove(this, ev, fn);
                    });
                    return;
                }
                el.removeEventListener(ev, fn, false);
            } : function(el, ev, fn) {
                if (Jelo.Valid.isArray(el)) {
                    Jelo.each(el, function() {
                        Jelo.Event.remove(this, ev, fn);
                    });
                    return;
                }
                var i = find(el, ev, fn);
                if (i == -1) {
                    return;
                }
                var d = el.document || el;
                var w = d.parentWindow;
                var hid = el._handlers[i];
                var h = w._allHandlers[hid];
                el.detachEvent('on' + ev, h.wrappedHandler);
                el._handlers.splice(i, 1);
                delete w._allHandlers[hid];
            };
            Jelo.on = Jelo.Event.add;
            Jelo.un = Jelo.Event.remove;
            Jelo.Event.isFixed = function() {
                return true;
            }
        },
        /**
         * @deprecated
         * True when {@link Jelo.Event.normalize} has been called. 
         */
        isFixed   : function() {
            return false;
        }
        
    };
}();

Jelo.Event.fix = Jelo.Event.normalize;

/**
 * Alias for {@link Jelo.Event.add}.
 * 
 * @function
 * @memberOf Jelo
 */
Jelo.on = Jelo.Event.add;

/**
 * Alias for {@link Jelo.Event.remove}.
 * 
 * @function
 * @memberOf Jelo
 */
Jelo.un = Jelo.Event.remove;

/**
 * Alias for {@link Jelo.Event.remove}.
 * 
 * @function
 * @memberOf Jelo.Event
 */
Jelo.Event.rem = Jelo.Event.remove;

/**
 * Alias for {@link Jelo.Event.onReady}.
 * 
 * @function
 * @memberOf Jelo
 */
Jelo.onReady = Jelo.Event.onReady;
